package com.linkgie.galaxyframework.utils.image;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.Random;

import javax.imageio.ImageIO;

/**
 * 具有随机噪声背景的图片；
 * 
 * @author huanghaiquan
 *
 */
public class NoiseImage {

	private static final double DISTORT_MODULUS = 6.2831853071795862D;

	private static final double DISTORT_X_FRAMES = 1;

	private static final double DISTORT_Y_FRAMES = 20;

	/**
	 * 背景噪点的噪声率；
	 */
	private static final float NOISE_PRATE = 0.08f;

	/**
	 * 文字正负方向旋转的最大弧度；
	 * <p>
	 * 
	 * 默认为 45 度；
	 */
	private static final double CHAR_ROTATION_RADIANS = 45 * Math.PI / 180;

	/**
	 * 背景RGB颜色的下限值；
	 */
	private static final int BACKGROUND_RGB_DOWN = 200;

	/**
	 * 背景RGB颜色的下限值；
	 */
	private static final int BACKGROUND_RGB_UP = 250;
	
	/**
	 * 前景文字RGB颜色的下限值；
	 */
	private static final int CHAR_RGB_DOWN = 80;

	/**
	 * 前景文字RGB颜色的下限值；
	 */
	private static final int CHAR_RGB_UP = 120;

	/**
	 * 是否在文字的 X 轴方向上使绘制的文字有随机的位置重叠，以增加混淆效果；
	 */
	private static final boolean X_OVERLAP = false;

	private final SecureRandom RANDOM;

	private final String CHARS;

	private final int WIDTH;

	private final int HEIGHT;

	private BufferedImage image;

	/**
	 * 字体大小；比高度小 4 ；
	 */
	private final int FONT_SIZE;

	/**
	 * 是否在扭曲背景图案之后重绘边框；
	 */
	private boolean redrawBorder = true;

	public String getChars() {
		return CHARS;
	}

	private NoiseImage(String chars, int width, int height, SecureRandom random) {
		this.CHARS = chars;
		this.WIDTH = width;
		this.HEIGHT = height;
		this.FONT_SIZE = height - 6;
		this.RANDOM = random;
	}

	/**
	 * 生成噪音图片;
	 * <p>
	 * 默认采用 {@link SecureRandom} 作为随机噪音发生器；
	 * 
	 * @param chars  作为前景的文字内容；
	 * @param width  图片宽；
	 * @param height 图片高；
	 * @return
	 */
	public static NoiseImage generate(String chars, int width, int height) {
		return generate(chars, width, height, new SecureRandom());
	}

	/**
	 * 生成噪音图片;
	 * 
	 * @param chars  作为前景的文字内容；
	 * @param width  图片宽；
	 * @param height 图片高；
	 * @param random 随机噪音发生器；
	 * @return
	 */
	public static NoiseImage generate(String chars, int width, int height, SecureRandom random) {
		NoiseImage image = new NoiseImage(chars, width, height, random);
		image.init();
		return image;
	}

	private void init() {
		this.image = generateImage();
	}

	/**
	 * 以指定的图片格式保存到文件；
	 * 
	 * @param format     图片格式；例如: png，jpg 等；
	 * @param outputFile
	 * @throws IOException
	 */
	public void saveAs(String format, File outputFile) {
		try {
			ImageIO.write(image, format, outputFile);
		} catch (IOException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	/**
	 * 以指定的图片格式保存；
	 * 
	 * @param format 图片格式；例如: png，jpg 等；
	 * @param output 输出流；
	 * @throws IOException
	 */
	public void saveAs(String format, OutputStream output) {
		try {
			ImageIO.write(image, format, output);
		} catch (IOException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	/**
	 * 以指定的图片格式保存；
	 * 
	 * @param format 图片格式；例如: png，jpg 等；
	 * @throws IOException
	 */
	public byte[] saveAs(String format) {
		try {
			ByteArrayOutputStream output = new ByteArrayOutputStream();
			ImageIO.write(image, format, output);
			return output.toByteArray();
		} catch (IOException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	/**
	 * 生成的验证码图片;
	 * 
	 * @param CHARS
	 * @param WIDTH
	 * @param HEIGHT
	 * @return
	 */
	private BufferedImage generateImage() {
		BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);

		Graphics2D board = image.createGraphics();
		board.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

		// 设置边框色
		board.setColor(Color.GRAY);
		board.fillRect(0, 0, WIDTH, HEIGHT);

		Color backgroundCollor = getRanddomColor(BACKGROUND_RGB_DOWN, BACKGROUND_RGB_UP, RANDOM);

		// 设置背景颜色；
		board.setColor(backgroundCollor);
		board.fillRect(0, 2, WIDTH, HEIGHT - 4);

		// 绘制干扰线条；
//		Random RANDOM = new Random(); 
		drawNoiseLines(board, WIDTH, HEIGHT, RANDOM);

		// 扭曲图案；
		distort(board, WIDTH, HEIGHT, backgroundCollor, redrawBorder, RANDOM);

		// 加入噪点；
		drawNoisePixels(image, WIDTH, HEIGHT, RANDOM);

		// 设置字体；
		Font font = Fonts.getSourceHanSans_CN_Normal(Font.ITALIC, FONT_SIZE);

		// 文字内容的颜色；
		Color fontColor = getRanddomColor(CHAR_RGB_DOWN, CHAR_RGB_UP, RANDOM);

		// 绘制文字，每个文字以随机旋转正负180度的方式绘制，
		drawCharsInRandomRotation(board, CHARS, font, FONT_SIZE, fontColor, WIDTH, HEIGHT, RANDOM);

		board.dispose();

		return image;
	}

	/**
	 * 绘制文字，每个文字以随机旋转正负180度的方式绘制;
	 * 
	 * @param board     绘制面板；
	 * @param chars     要绘制的字符串；
	 * @param font      字体；
	 * @param fontSize  字体大小；
	 * @param fontColor 字体颜色；
	 * @param width     绘制区域的宽度；
	 * @param height    绘制区域的高度；
	 * @param RANDOM    旋转变换的随机数发生器；
	 */
	private static void drawCharsInRandomRotation(Graphics2D board, String chars, Font font, int fontSize,
			Color fontColor, int width, int height, Random random) {
		// 设置字体；
		board.setFont(font);
		// 文字内容的颜色；
		board.setColor(fontColor);

		char[] charArray = chars.toCharArray();
		final int CHARS_LENGTH = charArray.length;

		final int ROTATION_ANCHOR_X_STEP = width / CHARS_LENGTH;
		final int ROTATION_ANCHOR_X_OFFSET = fontSize / 2;
		final int ROTATION_ANCHOR_Y = height / 2;

		final int BASELINE_X_STEP = (width - 10) / CHARS_LENGTH;
		final int BASELINE_X_OFFSET = 5;
		final int BASELINE_Y = height / 2 + fontSize / 2 - 5;

		// 打印文字内容;
		// 将每个文字都随机旋转正负 90 度(注：以弧度为单位，360度圆周的弧度为 2π)；
		// 相邻的文字在 X 轴方向有随机的重叠宽度，随机范围为半个字符显示区宽度；
		for (int i = 0; i < CHARS_LENGTH; i++) {
			AffineTransform transform = new AffineTransform();
			transform.setToRotation(CHAR_ROTATION_RADIANS * random.nextDouble() * (random.nextBoolean() ? 1 : -1),
					ROTATION_ANCHOR_X_STEP * i + ROTATION_ANCHOR_X_OFFSET, ROTATION_ANCHOR_Y);
			board.setTransform(transform);

			if (X_OVERLAP) {
				board.drawChars(charArray, i, 1,
						BASELINE_X_STEP * i + BASELINE_X_OFFSET + (int) (BASELINE_X_STEP / 2 * random.nextDouble()),
						BASELINE_Y);
			} else {
				board.drawChars(charArray, i, 1, BASELINE_X_STEP * i + BASELINE_X_OFFSET, BASELINE_Y);
			}
		}
	}

	/**
	 * @param image
	 * @param maxX        噪点位置的最大 X 坐标；从 0 开始；
	 * @param maxY        噪点位置的最大 Y 坐标；从 0 开始；
	 * @param noiseRandom 随机噪音生成器；
	 */
	private static void drawNoisePixels(BufferedImage image, int maxX, int maxY, Random noiseRandom) {
		int area = (int) (NOISE_PRATE * maxX * maxY);
		for (int i = 0; i < area; i++) {
			int x = noiseRandom.nextInt(maxX);
			int y = noiseRandom.nextInt(maxY);
			int rgb = getRandomRgb(noiseRandom);
			image.setRGB(x, y, rgb);
		}
	}

	private static void drawNoiseLines(Graphics2D board, int maxX, int maxY, Random noiseRandom) {
		// 设置干扰线条的颜色
		board.setColor(getRanddomColor(160, 200, noiseRandom));
		for (int i = 0; i < 20; i++) {
			int x = noiseRandom.nextInt(maxX - 1);
			int y = noiseRandom.nextInt(maxY - 1);
			int xl = noiseRandom.nextInt(6) + 1;
			int yl = noiseRandom.nextInt(12) + 1;
			board.drawLine(x, y, x + xl + 40, y + yl + 20);
		}
	}

	/**
	 * 获取随机颜色；
	 * 
	 * @param rgbDown 随机值下限；值范围 [0 ~ 255] ；
	 * @param rgbUp   随机值上限；值范围 [0 ~ 255] ；
	 * @param random  随机数发生器；
	 * @return
	 */
	private static Color getRanddomColor(int rgbDown, int rgbUp, Random random) {
		if (rgbDown > 255) {
			throw new IllegalArgumentException("The down limit of color is out of bound!");
		}
		if (rgbUp > 255) {
			throw new IllegalArgumentException("The up limit of color is out of bound!");
		}
		final int RANGE = rgbUp - rgbDown;
		int r = rgbDown + random.nextInt(RANGE);
		int g = rgbDown + random.nextInt(RANGE);
		int b = rgbDown + random.nextInt(RANGE);
		return new Color(r, g, b);
	}

	private static int getRandomRgb(Random colorRandom) {
		int rgbByte;
		int rgb = 0;
		for (int i = 0; i < 3; i++) {
			rgbByte = colorRandom.nextInt(255);
			rgb = (rgb << 8) | rgbByte;
		}
		return rgb;
	}

	/**
	 * 扭曲图案；
	 * 
	 * @param board
	 * @param WIDTH
	 * @param HEIGHT
	 * @param color
	 * @param redrawBorder     在扭曲图案之后是否重新描绘图片边框；
	 * @param coordinateRandom
	 */
	private static void distort(Graphics board, int width, int height, Color color, boolean redrawBorder,
			Random coordinateRandom) {
		distort_X(board, width, height, color, redrawBorder, coordinateRandom);
		distort_Y(board, width, height, color, redrawBorder, coordinateRandom);
	}

	private static void distort_X(Graphics board, int width, int height, Color color, boolean redrawBorder,
			Random coordinateRandom) {
		int period = coordinateRandom.nextInt(2);
		int phase = coordinateRandom.nextInt(2);

		for (int i = 0; i < height; i++) {
			double d = (double) (period >> 1)
					* Math.sin((double) i / (double) period + (DISTORT_MODULUS * (double) phase) / DISTORT_X_FRAMES);
			board.copyArea(0, i, width, 1, (int) d, 0);
			if (redrawBorder) {
				board.setColor(color);
				board.drawLine((int) d, i, 0, i);
				board.drawLine((int) d + width, i, width, i);
			}
		}

	}

	private static void distort_Y(Graphics board, int width, int height, Color color, boolean redrawBorder,
			Random coordinateRandom) {

		final int phase = 7;
		int period = coordinateRandom.nextInt(40) + 10; // 50;
		for (int i = 0; i < width; i++) {
			double d = (double) (period >> 1)
					* Math.sin((double) i / (double) period + (DISTORT_MODULUS * (double) phase) / DISTORT_Y_FRAMES);
			board.copyArea(i, 0, 1, height, 0, (int) d);
			if (redrawBorder) {
				board.setColor(color);
				board.drawLine(i, (int) d, i, 0);
				board.drawLine(i, (int) d + height, i, height);
			}

		}
	}

}
